Skip to content

fix(platform): reject path traversal and harden fs cache and retry boundaries - #3315

Merged
kojiwakayama merged 5 commits into
mainfrom
fix/github-fs-path-hardening
Aug 3, 2026
Merged

fix(platform): reject path traversal and harden fs cache and retry boundaries#3315
kojiwakayama merged 5 commits into
mainfrom
fix/github-fs-path-hardening

Conversation

@kojiwakayama

@kojiwakayama kojiwakayama commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Ports five verified security/robustness fixes from codex/module-reconcile-20260723 onto main via per-file checkouts and hand-ported hunks (no merge). Every hunk was audited against main to exclude the branch's unrelated changes and reverts.

C1 — Path traversal defense (GitHub + Veryfront fs adapters)

Traversal vector. Main's normalizeGitHubPath performed no .. rejection. The normalized path flows into github-api-client.ts, which builds /repos/${owner}/${repo}/contents/${path} and fetches "https://api.github.com" + endpoint. WHATWG URL resolution collapses dot segments, so an input like ../../../../user/repos escapes the repo scope and becomes an arbitrary token-authenticated GitHub API request. Reachable via readTextFilereadContentsFile whenever the tree index lacks the path. PathNormalizer.normalize (Veryfront adapter) had the identical gap for paths sent to the Veryfront API.

Segment-boundary bug. Both normalizers stripped projectDir with a bare startsWith(projectDir), so projectDir /project/root wrongly matched /project/root-other/.... Both now strip only at a complete path-segment boundary.

What is rejected unconditionally: .. segments (both adapters); control characters, backslashes, and >4096-char paths (Veryfront PathNormalizer, including its projectDir at construction).

.-segment decision: normalize away, do not throw. The reconcile branch also threw on . segments; this PR intentionally drops that and silently normalizes . away instead. Evidence:

  • src/discovery/module-import.ts:20 treats projectDir === "." as a valid, expected value (and defaults baseDir to "."), so . is a live convention around adapter boundaries; throwing on it risks breaking legitimate configurations (e.g. GitHub adapter projectDir: "." now normalizes to "no project dir" instead of throwing).
  • The module-resolution pipeline (resolveRelative, src/transforms/esm/import-parser.ts:360) collapses ./.. before calling the adapters, and HTTP-derived pathnames are WHATWG-normalized — so rejecting . buys no security (it aliases nothing), while normalizing it is strictly safer than main's behavior of passing . through untouched.

Throwing-behavior change. normalizeGitHubPath and PathNormalizer.normalize previously never threw; they now throw TypeError on hostile input. All main call sites audited (adapter.ts, base-operations.ts, read-operations.ts, stat-operations.ts, directory-operations.ts in both adapters): every call happens inside adapter methods that already propagate errors (e.g. FILE_NOT_FOUND from stat), so a hostile path now surfaces as a rejected promise on the same channel. GitHub exists() catches all errors and returns false; Veryfront exists() rethrows non-not-found errors, so hostile input rejects instead of silently reporting false — intended. PathNormalizer's constructor now validates projectDir (fail-fast at adapter construction from config).

C2 — size-estimator.ts guarded serialization

Main ran JSON.stringify(value).length * 2 bare, so cyclic values, BigInts, or throwing toJSON hooks propagated exceptions out of FileCache.set(). The ported version returns Number.MAX_SAFE_INTEGER (uncacheable, rejected by admission limits) for values that cannot be serialized. Zero dependencies.

C3 — Repo-scoped GitHub cache keys (cache-scope.ts)

Main keyed GitHub cache entries on the bare ref, so two repos with identical paths and refs collided in a shared cache. New buildGitHubCacheRef() scopes keys to URI-encoded owner:repo:ref. Wired into the minimal set of key-builder call sites:

  • directory-operations.ts (readdir key)
  • stat-operations.ts (stat + resolve keys) — hand-ported hunks only; the branch's unrelated index-generation/async-cache/tree-validation rewrite and its removal of main's symlink-skip were excluded
  • read-operations.ts (content, bytes, and bounded-read exact keys) — hand-edited on top of main, not taken from the branch: the branch version deletes main's readFileBytesWithinLimit (bounded SHA-pinned reads). Verified the diff vs main is exactly the cache-scope.ts import plus four key expressions; every main symbol and the sync cache shape are preserved verbatim.

Regression tests: cache-scope.test.ts (branch), a cross-repo readdir isolation test (branch), and a new cross-repo readTextFile content-isolation test pinning the collision fix.

Retry-boundary hardening (added from the residual audit)

  • fs/veryfront/retry.ts: transient-error classification previously accepted any status >= 500 (600, Infinity) and invoked arbitrary getters (.status, .message) on attacker-shaped throwables. Ported branch version uses getOwnPropertyDescriptor-based data reads, native-error/proxy checks (isNativeErrorWithoutHooks / isProxyWithoutHooks, already on main in platform/compat/error-introspection.ts), and an integer 500–599 window. Branch tests ported.
  • fs/veryfront/adapter-helpers.ts buildRetryConfig: main spread caller retry config with zero validation ({maxRetries: 500}, {initialDelay: NaN} passed through). It now routes through main's existing normalizeFilesystemRetryConfig (src/utils/config-resource-limits.ts), documented for exactly this boundary.
  • fs/veryfront/types.ts: the veryfront.retry override shape declared a vestigial retryDelay field consumed nowhere (the client takes initialDelay/maxDelay); replaced with initialDelay?/maxDelay? so delay overrides are actually expressible and validated. This also fixes a pre-existing deno check failure in adapter-helpers.test.ts on main (it already passed initialDelay).

Merge gate

PASS — script-compared it(/Deno.test names between origin/main and this branch for every touched test file: no main test name disappears; all changes are additive (plus new tests).

Verification

  • deno check on all 19 changed files: pass (note: adapter-helpers.test.ts fails deno check on origin/main today; fixed here by the types.ts correction)
  • deno lint / deno fmt --check on changed files: pass
  • VF_DISABLE_LRU_INTERVAL=1 NODE_ENV=production LOG_FORMAT=text deno test --preload=src/schemas/_test-setup.ts --no-check --allow-all --unstable-worker-options --unstable-net src/platform/adapters: 139 passed (1851 steps), 0 failed
  • Regression tests prove ../../../../user/repos-style input throws before any URL construction in both adapters

The second commit (chore: drop unused stringifyJsonValue imports to unblock pre-push lint) removes two unused imports in the LLM extension request builders that fail the pre-push lint gate on current main.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented cache entries from being shared across different repositories.
    • Improved path normalization and blocked traversal, invalid characters, and unsafe inputs.
    • Made size estimation resilient to cyclic data, BigInt, and serialization errors.
    • Improved retry handling for network failures and invalid or unsafe errors.
  • New Features

    • Added configurable initial and maximum retry delays.
    • Added stricter retry limits and validation for retry settings.
  • Tests

    • Expanded coverage for cache isolation, path safety, serialization, and retry behavior.

Breaking-change migration note

This repository has no standalone changelog file; this section is the release/migration record for the exported filesystem adapter contract changed by this PR.

  • fs.veryfront.retry.maxRetries must now be an integer from 0 through MAX_VERYFRONT_FILESYSTEM_RETRIES (currently 9).
  • initialDelay and maxDelay must be finite non-negative values, and initialDelay must not exceed maxDelay. Invalid values now fail application startup with config-validation-failed instead of being accepted or silently switching to the local filesystem.
  • The exported but previously unused retryDelay field was removed. Replace it with initialDelay and, when needed, maxDelay.
  • Operators should audit deployed fs.veryfront.retry values before upgrading. Repository code contains no remaining retryDelay usages; deployment-specific configuration must be checked in the deployment/control-plane source of truth.

The fail-fast behavior is intentional: invalid remote-filesystem configuration must not degrade to the host-local filesystem.

Copilot AI review requested due to automatic review settings August 3, 2026 08:13
@kojiwakayama
kojiwakayama requested a review from kwakayama as a code owner August 3, 2026 08:13
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kojiwakayama, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 82d13bfd-bcb9-4c25-89d5-104596f819e5

📥 Commits

Reviewing files that changed from the base of the PR and between 2da077d and ed1a04d.

📒 Files selected for processing (8)
  • src/platform/adapters/README.md
  • src/platform/adapters/fs/integration.test.ts
  • src/platform/adapters/fs/integration.ts
  • src/platform/adapters/fs/veryfront/adapter-helpers.test.ts
  • src/platform/adapters/fs/veryfront/adapter-helpers.ts
  • src/platform/adapters/fs/veryfront/path-normalizer.test.ts
  • src/platform/adapters/fs/veryfront/path-normalizer.ts
  • src/platform/adapters/fs/veryfront/types.ts
📝 Walkthrough

Walkthrough

The pull request hardens filesystem path and retry handling, scopes GitHub cache keys by repository, handles failed object serialization safely, updates retry configuration, and removes two unused imports.

Changes

Cache behavior

Layer / File(s) Summary
Safe object-size estimation
src/platform/adapters/fs/cache/size-estimator.ts, src/platform/adapters/fs/cache/size-estimator.test.ts
Object serialization failures and undefined results return Number.MAX_SAFE_INTEGER. Tests cover cycles, BigInt, throwing toJSON, and omitted values.
Repository-scoped GitHub caching
src/platform/adapters/fs/github/cache-scope.ts, src/platform/adapters/fs/github/*-operations.ts, src/platform/adapters/fs/github/*-operations.test.ts
Cache references encode the owner, repository, and ref. Read, directory, stat, and file-resolution caches use the derived reference.
GitHub path normalization
src/platform/adapters/fs/github/path-utils.ts, src/platform/adapters/fs/github/path-utils.test.ts
Path normalization removes current-directory segments, rejects traversal, and applies project-directory stripping only at exact or path boundaries.

Veryfront adapter validation

Layer / File(s) Summary
Retry configuration normalization
src/platform/adapters/fs/veryfront/types.ts, src/platform/adapters/fs/veryfront/adapter-helpers.ts, src/platform/adapters/fs/veryfront/adapter-helpers.test.ts
Retry settings use initialDelay and maxDelay. Retry counts and delay combinations are validated.
Veryfront path validation
src/platform/adapters/fs/veryfront/path-normalizer.ts, src/platform/adapters/fs/veryfront/path-normalizer.test.ts
PathNormalizer validates traversal, backslashes, control characters, input length, and project-directory boundaries.
Safe transient-error handling
src/platform/adapters/fs/veryfront/retry.ts, src/platform/adapters/fs/veryfront/retry.test.ts
Retry detection safely handles hostile objects, native fetch TypeError values, and integer HTTP 5xx statuses. Retry logging uses safe message extraction.

Request-builder cleanup

Layer / File(s) Summary
Unused import removal
extensions/ext-llm-anthropic/src/anthropic-request-builder.ts, extensions/ext-llm-openai/src/openai-responses-request-builder.ts
Removed unused stringifyJsonValue imports. Request-building behavior is unchanged.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHubReadOperations
  participant buildGitHubCacheRef
  participant FileCache
  GitHubReadOperations->>buildGitHubCacheRef: Build encoded owner, repo, and ref scope
  buildGitHubCacheRef-->>GitHubReadOperations: Return cache reference
  GitHubReadOperations->>FileCache: Read or write content using scoped path key
Loading

Possibly related PRs

Suggested reviewers: kwakayama, copilot, ariskemper

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main security and robustness fixes for path traversal, filesystem caching, and retry handling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/github-fs-path-hardening

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the filesystem adapters (GitHub + Veryfront API) against path traversal, cache-collision, unsafe error introspection, and retry-boundary misuse, and adds regression tests to lock in the security/robustness fixes.

Changes:

  • Reject .. traversal segments (while normalizing away .) and fix projectDir prefix stripping to only match complete path-segment boundaries.
  • Scope GitHub cache keys by owner:repo:ref to prevent cross-repo cache collisions, plus add isolation tests.
  • Make FS cache sizing resilient to non-serializable values, and harden retry transient-error detection to avoid invoking attacker-controlled getters.

Verification

  • I did not run commands in this review environment. Safest next step: run the adapter-focused suite mentioned in the PR description (plus deno lint / deno fmt --check on changed files) after addressing the review comments.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/platform/adapters/fs/veryfront/types.ts Updates retry override type shape to match validated delay fields.
src/platform/adapters/fs/veryfront/retry.ts Hardens transient-error classification and avoids unsafe inspection patterns.
src/platform/adapters/fs/veryfront/retry.test.ts Adds regression tests for hostile throwables and status validation.
src/platform/adapters/fs/veryfront/path-normalizer.ts Adds path safety checks and rejects traversal segments in normalized paths.
src/platform/adapters/fs/veryfront/path-normalizer.test.ts Adds tests for traversal rejection, boundary stripping, and invalid characters/length.
src/platform/adapters/fs/veryfront/adapter-helpers.ts Validates/normalizes retry config via shared resource-limit utility.
src/platform/adapters/fs/veryfront/adapter-helpers.test.ts Adds tests for retry budget and delay validation at adapter construction.
src/platform/adapters/fs/github/stat-operations.ts Scopes stat/resolve cache keys by repo identity.
src/platform/adapters/fs/github/read-operations.ts Scopes content/bytes cache keys by repo identity.
src/platform/adapters/fs/github/read-operations.test.ts Adds cross-repo content cache isolation test.
src/platform/adapters/fs/github/path-utils.ts Rejects traversal segments and fixes projectDir stripping boundary logic.
src/platform/adapters/fs/github/path-utils.test.ts Adds tests for traversal rejection, . normalization, and boundary stripping.
src/platform/adapters/fs/github/directory-operations.ts Scopes directory cache keys by repo identity.
src/platform/adapters/fs/github/directory-operations.test.ts Adds cross-repo directory cache isolation test.
src/platform/adapters/fs/github/cache-scope.ts Introduces repo-scoped ref builder for GitHub cache keys.
src/platform/adapters/fs/github/cache-scope.test.ts Tests repo scoping and delimiter encoding.
src/platform/adapters/fs/cache/size-estimator.ts Treats non-serializable objects as uncacheable instead of throwing.
src/platform/adapters/fs/cache/size-estimator.test.ts Adds tests for cyclic/BigInt/throwing-toJSON serialization cases.
extensions/ext-llm-openai/src/openai-responses-request-builder.ts Removes an unused import to satisfy lint.
extensions/ext-llm-anthropic/src/anthropic-request-builder.ts Removes an unused import to satisfy lint.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/platform/adapters/fs/veryfront/retry.ts
Comment thread src/platform/adapters/fs/veryfront/path-normalizer.ts
Copilot AI review requested due to automatic review settings August 3, 2026 08:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/platform/adapters/fs/veryfront/types.ts:131

  • FSAdapterConfig.veryfront.retry removed retryDelay and added initialDelay/maxDelay, but the public adapter documentation still shows the old retryDelay field. This can mislead users configuring the adapter and conflicts with the runtime validation schema (which expects initialDelay/maxDelay). Update src/platform/adapters/README.md's FSAdapterConfig snippet to match this interface.
      ttl?: number;
    };
    retry?: {
      /** Retries after the initial request, from 0 through 9. */
      maxRetries?: number;
      initialDelay?: number;
      maxDelay?: number;
    };

Copilot AI review requested due to automatic review settings August 3, 2026 08:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/platform/adapters/fs/veryfront/types.ts:127

  • The JSDoc hard-codes an upper bound ("0 through 9"). The actual maximum is derived from constants in #veryfront/utils/config-resource-limits.ts and could change (e.g., if the API retry budget changes), causing this comment to become incorrect. Prefer wording that does not embed a specific number.
      /** Retries after the initial request, from 0 through 9. */

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
src/platform/adapters/fs/veryfront/retry.ts (2)

84-116: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the duplicated network error check.

Line 90 tests message.includes("network error") inside the isNativeTypeError branch. Line 112 tests the same substring for every error. Line 90 can never change the result, because any message that matches line 90 also matches line 112. The two comments also describe the same string differently, which is confusing.

Delete the check at line 90 and keep the shared check at line 112.

♻️ Proposed refactor
       message.includes("fetch failed") || // Deno runtime fetch failure
       message.includes("Failed to fetch") || // browser/undici fetch failure
       message.includes("error sending request") ||
-      message.includes("NetworkError when attempting to fetch") ||
-      message.includes("network error") // documented Fetch API network error string
+      message.includes("NetworkError when attempting to fetch")
     ) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/adapters/fs/veryfront/retry.ts` around lines 84 - 116, Remove
the redundant message.includes("network error") condition and its associated
comment from the isNativeTypeError branch. Keep the shared network error check
and explanatory comment in the broader retry classification condition unchanged.

136-138: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Log the throwable type when the safe message is empty.

getSafeErrorMessage returns "" for a non-native throwable, for example a plain object or a hostile proxy. The warning then records an empty error field and gives no signal about what failed. Add the typeof value as a fallback so the log stays diagnosable.

♻️ Proposed refactor
     onRetry: ({ error }) => {
+      const message = getSafeErrorMessage(error);
       logger.warn(`${context}: transient error, retrying once`, {
-        error: getSafeErrorMessage(error),
+        error: message || `<non-native throwable: ${typeof error}>`,
       });
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/adapters/fs/veryfront/retry.ts` around lines 136 - 138, Update
the warning payload in the retry flow around getSafeErrorMessage so an empty
safe error message falls back to the throwable’s typeof value. Preserve the
existing safe message when it is non-empty and ensure the error field always
provides this diagnostic fallback for non-native throwables.
src/platform/adapters/fs/veryfront/retry.test.ts (1)

175-198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fix the test title and use a strict identity assertion.

Two points:

  1. The title states "contains hostile throwable introspection hooks". The test verifies the opposite behavior: the retry path does not invoke the hostile traps and rethrows the value unchanged. Rename the test to describe the asserted behavior.
  2. Line 197 compares a boolean. assertStrictEquals(caught, hostile) states the intent directly and produces a useful diff on failure.
♻️ Proposed refactor
-    it("contains hostile throwable introspection hooks", async () => {
+    it("does not invoke hostile throwable introspection hooks", async () => {
       let callCount = 0;
       const hostile = new Proxy({}, {
         getOwnPropertyDescriptor(): never {
           throw new Error("descriptor trap");
         },
         get(): never {
           throw new Error("get trap");
         },
       });
 
       let caught: unknown;
       try {
         await withRetryOnTransient(() => {
           callCount++;
           throw hostile;
         }, "test");
       } catch (error) {
         caught = error;
       }
 
       assertEquals(callCount, 1);
-      assertEquals(caught === hostile, true);
+      assertStrictEquals(caught, hostile);
     });

Add assertStrictEquals to the existing import from #veryfront/testing/assert.ts.

As per coding guidelines: "use assertions from `#veryfront/testing/assert.ts`".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/adapters/fs/veryfront/retry.test.ts` around lines 175 - 198,
Update the test title in the hostile throwable test to describe that the value
is rethrown unchanged without triggering introspection traps. Import
assertStrictEquals from `#veryfront/testing/assert.ts` and replace the boolean
comparison of caught and hostile with a direct strict identity assertion,
preserving the existing call-count check.

Source: Coding guidelines

src/platform/adapters/fs/veryfront/adapter-helpers.test.ts (1)

46-62: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Identify the failing case in the loop and separate the accepting assertion.

Two points:

  1. The loop asserts four inputs without a case label. If one input stops throwing, the failure output does not show which input failed. Pass a message to assertThrows.
  2. Lines 57-61 assert that zero delays are accepted. The test title states rejection only. Move that assertion into its own it() block.
♻️ Proposed refactor
   it("rejects invalid retry delays at direct adapter construction", () => {
     for (
       const retry of [
         { initialDelay: -1 },
         { initialDelay: 0.5 },
         { maxDelay: MAX_TIMER_DELAY_MS + 1 },
         { initialDelay: 2, maxDelay: 1 },
       ]
     ) {
-      assertThrows(() => buildRetryConfig(retry), RangeError);
+      assertThrows(
+        () => buildRetryConfig(retry),
+        RangeError,
+        undefined,
+        `expected RangeError for ${JSON.stringify(retry)}`,
+      );
     }
+  });
+
+  it("accepts zero initial and maximum retry delays", () => {
     assertEquals(buildRetryConfig({ initialDelay: 0, maxDelay: 0 }), {
       maxRetries: DEFAULT_MAX_RETRIES,
       initialDelay: 0,
       maxDelay: 0,
     });
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/adapters/fs/veryfront/adapter-helpers.test.ts` around lines 46 -
62, Update the retry-delay tests around buildRetryConfig: pass each retry input
as the failure message to assertThrows so the failing case is identifiable, and
move the zero-delay acceptance assertion into a separate it() test with a title
describing accepted zero delays.
src/platform/adapters/fs/github/read-operations.test.ts (1)

5-5: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Import FileCache from #veryfront/cache. #veryfront/cache resolves to src/cache/index.ts, which exports FileCache.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/adapters/fs/github/read-operations.test.ts` at line 5, Update
the FileCache import in the test to use the `#veryfront/cache` alias, which
resolves through src/cache/index.ts and exports FileCache, instead of the
relative cache path.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/platform/adapters/fs/veryfront/adapter-helpers.ts`:
- Around line 20-28: Update buildRetryConfig to catch validation failures from
normalizeFilesystemRetryConfig and wrap them in a VeryfrontError with slug
"config-validation-failed"; preserve and rethrow existing matching
VeryfrontError instances using the specified instanceof and slug check. Ensure
createFSAdapterFromConfig propagates this registered error and
enhanceAdapterWithFS does not silently fall back to the local filesystem when
retry configuration is invalid.

In `@src/platform/adapters/fs/veryfront/path-normalizer.ts`:
- Around line 17-20: Update the constructor and normalization flow around
projectDirPrefix and normalize so both the configured project directory and
input path are canonicalized before project-prefix boundary comparison,
including removal of "." segments while preserving "/" root handling. Keep the
public API unchanged, and add a focused regression test covering canonical and
dot-segment forms of the same configured project directory.

In `@src/platform/adapters/fs/veryfront/types.ts`:
- Around line 126-131: Update the retry documentation in the retry configuration
type to reference MAX_VERYFRONT_FILESYSTEM_RETRIES instead of hardcoding 9,
keeping the documented range accurate if the constant changes. Remove retryDelay
from the filesystem adapter README and document the current FSAdapterConfig
fields initialDelay and maxDelay instead.

---

Nitpick comments:
In `@src/platform/adapters/fs/github/read-operations.test.ts`:
- Line 5: Update the FileCache import in the test to use the `#veryfront/cache`
alias, which resolves through src/cache/index.ts and exports FileCache, instead
of the relative cache path.

In `@src/platform/adapters/fs/veryfront/adapter-helpers.test.ts`:
- Around line 46-62: Update the retry-delay tests around buildRetryConfig: pass
each retry input as the failure message to assertThrows so the failing case is
identifiable, and move the zero-delay acceptance assertion into a separate it()
test with a title describing accepted zero delays.

In `@src/platform/adapters/fs/veryfront/retry.test.ts`:
- Around line 175-198: Update the test title in the hostile throwable test to
describe that the value is rethrown unchanged without triggering introspection
traps. Import assertStrictEquals from `#veryfront/testing/assert.ts` and replace
the boolean comparison of caught and hostile with a direct strict identity
assertion, preserving the existing call-count check.

In `@src/platform/adapters/fs/veryfront/retry.ts`:
- Around line 84-116: Remove the redundant message.includes("network error")
condition and its associated comment from the isNativeTypeError branch. Keep the
shared network error check and explanatory comment in the broader retry
classification condition unchanged.
- Around line 136-138: Update the warning payload in the retry flow around
getSafeErrorMessage so an empty safe error message falls back to the throwable’s
typeof value. Preserve the existing safe message when it is non-empty and ensure
the error field always provides this diagnostic fallback for non-native
throwables.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 553ea7ab-ee44-4dd4-9072-2891fd9af956

📥 Commits

Reviewing files that changed from the base of the PR and between cf9b546 and 2da077d.

📒 Files selected for processing (21)
  • extensions/ext-llm-anthropic/src/anthropic-request-builder.ts
  • extensions/ext-llm-openai/src/openai-responses-request-builder.ts
  • scripts/lint/test-typecheck-baseline.json
  • src/platform/adapters/fs/cache/size-estimator.test.ts
  • src/platform/adapters/fs/cache/size-estimator.ts
  • src/platform/adapters/fs/github/cache-scope.test.ts
  • src/platform/adapters/fs/github/cache-scope.ts
  • src/platform/adapters/fs/github/directory-operations.test.ts
  • src/platform/adapters/fs/github/directory-operations.ts
  • src/platform/adapters/fs/github/path-utils.test.ts
  • src/platform/adapters/fs/github/path-utils.ts
  • src/platform/adapters/fs/github/read-operations.test.ts
  • src/platform/adapters/fs/github/read-operations.ts
  • src/platform/adapters/fs/github/stat-operations.ts
  • src/platform/adapters/fs/veryfront/adapter-helpers.test.ts
  • src/platform/adapters/fs/veryfront/adapter-helpers.ts
  • src/platform/adapters/fs/veryfront/path-normalizer.test.ts
  • src/platform/adapters/fs/veryfront/path-normalizer.ts
  • src/platform/adapters/fs/veryfront/retry.test.ts
  • src/platform/adapters/fs/veryfront/retry.ts
  • src/platform/adapters/fs/veryfront/types.ts
💤 Files with no reviewable changes (3)
  • extensions/ext-llm-openai/src/openai-responses-request-builder.ts
  • extensions/ext-llm-anthropic/src/anthropic-request-builder.ts
  • scripts/lint/test-typecheck-baseline.json

Comment thread src/platform/adapters/fs/veryfront/adapter-helpers.ts Outdated
Comment thread src/platform/adapters/fs/veryfront/path-normalizer.ts Outdated
Comment thread src/platform/adapters/fs/veryfront/types.ts
Copilot AI review requested due to automatic review settings August 3, 2026 08:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/platform/adapters/fs/veryfront/adapter-helpers.ts:35

  • The catch path uses error instanceof VeryfrontError and then reads error.slug directly. In this codebase, proxies can satisfy instanceof VeryfrontError and still throw from field access (see src/errors/types.ts snapshot helpers), which can turn a config-validation error check into a secondary exception and change fallback behavior. Prefer a side-effect-free slug read (via own-property descriptor) guarded by try/catch.
  } catch (error) {
    if (error instanceof VeryfrontError && error.slug === "config-validation-failed") {
      throw error;
    }

src/platform/adapters/fs/integration.ts:69

  • This catch block uses error instanceof VeryfrontError and then reads error.slug. A hostile/proxied throwable can satisfy instanceof and throw from property access, causing the error-fallback logic itself to throw and potentially skip logging/fallback. Use a side-effect-free slug read (own-property descriptor) guarded by try/catch before deciding to rethrow.
      } catch (error) {
        if (error instanceof VeryfrontError && error.slug === "config-validation-failed") {
          throw error;
        }

@kojiwakayama
kojiwakayama added this pull request to the merge queue Aug 3, 2026

@kwakayama kwakayama left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: 54/100 — request changes

Axis Score
Correctness 18/40
Test adequacy 14/25
Security / prod-safety 9/20
Maintainability 11/15
Total 54/100

Merge base c21affb69c5b, head ed1a04d1d. All diffs three-dot from the explicit merge base.

The vulnerability is real and correctly diagnosed. The fix is incomplete: the guard blocks only the literal .. form, and I empirically confirmed four inputs that produce the exact escape the PR says it closes. Main is fully open, so this is a net improvement — but the PR body and its tests assert a completeness that does not exist, which is the "incomplete guard creates false confidence" case.

P0 — traversal guard bypassable via percent-encoded dot segments and backslashes

src/platform/adapters/fs/github/path-utils.ts:23-31

Confidence: high — empirically executed against the real endpoint-construction logic.

The sink at github-api-client.ts:61-63 interpolates the path raw:

const normalizedPath = path.replace(/^\/+/, "");
const endpoint = `/repos/${this.config.owner}/${this.config.repo}/contents/${normalizedPath}?ref=${contentRef}`;

There is no encodeURIComponent anywhere in that file (verified by grep). github-api-client.ts:131 then does new URL(...) and fetch, with the auth header attached at :176.

The guard rejects a segment only when it is exactly ... But the WHATWG URL Standard defines a double-dot path segment as .., .%2e, %2e., or %2e%2e (ASCII case-insensitive), and treats \ as a path separator for special schemes. I reproduced the real sink and got:

blocked       | escaped=YES | "../../../../user/repos"                  -> https://api.github.com/user/repos?ref=main
PASSES-GUARD  | escaped=YES | "%2e%2e/%2e%2e/%2e%2e/%2e%2e/user/repos"  -> https://api.github.com/user/repos?ref=main
PASSES-GUARD  | escaped=YES | "%2E%2E/%2E%2E/%2E%2E/%2E%2E/user/repos"  -> https://api.github.com/user/repos?ref=main
PASSES-GUARD  | escaped=YES | ".%2e/.%2e/.%2e/.%2e/user/repos"          -> https://api.github.com/user/repos?ref=main
PASSES-GUARD  | escaped=YES | "..\..\..\..\user/repos"                  -> https://api.github.com/user/repos?ref=main
blocked       | escaped=no  | "docs/readme.md"                          -> .../contents/docs/readme.md?ref=main

Concrete failure: caller invokes adapter.readTextFile("%2e%2e/%2e%2e/%2e%2e/%2e%2e/user/repos"). normalizeGitHubPath returns it unchanged (no segment equals ..). read-operations.ts:40 passes it to readContentsFile → the URL resolves to https://api.github.com/user/repos?ref=main carrying Authorization: Bearer <GITHUB_TOKEN>. The full repo list visible to that token is returned to the caller as "file contents". Substituting /user, /orgs/{org}/members, etc. reaches any GET the token can perform. Reachability is exactly as the PR states for the literal form: readTextFilereadContentsFile whenever the tree index lacks the path.

The fix belongs at the sink, and it needs BOTH layers. I tested the encoding fix and it does not subsume the .. rejection:

# segment-wise encodeURIComponent at github-api-client.ts:62
ESCAPED !! | "../../../../user/repos"                 -> https://api.github.com/user/repos?ref=main
CONTAINED  | "%2e%2e/%2e%2e/%2e%2e/%2e%2e/user/repos" -> .../contents/%252e%252e/%252e%252e/...
CONTAINED  | "%2E%2E/..."                             -> .../contents/%252E%252E/...
CONTAINED  | ".%2e/.%2e/..."                          -> .../contents/.%252e/.%252e/...
CONTAINED  | "..\..\..\..\user/repos"                 -> .../contents/..%5C..%5C..%5C..%5Cuser/repos
ok         | "docs/read me.md"                        -> .../contents/docs/read%20me.md
ok         | "src/a#b.ts"                             -> .../contents/src/a%23b.ts

encodeURIComponent("..") returns ".." unchanged — dots are unreserved characters. So encoding closes the encoded and backslash variants but leaves the literal ../ escape wide open, and the existing .. rejection closes the literal form but nothing else. Each layer covers exactly what the other misses.

Recommended: apply path.split("/").map(encodeURIComponent).join("/") at github-api-client.ts:62 and keep the .. segment rejection — it is load-bearing, not defence in depth. Bonus: encoding also fixes paths containing spaces, #, and ?, which are broken on main today.

P1 — the GitHub normalizer omits the control-char/backslash/length checks the Veryfront one has

src/platform/adapters/fs/github/path-utils.ts

The PR body states this asymmetry as intentional. But the GitHub adapter is the one whose sink is unencoded, so it needs them more. PathNormalizer.assertSafePath (path-normalizer.ts:79-98) rejects \; normalizeGitHubPath does not — which is exactly why the ..\..\ bypass works against GitHub and not against Veryfront.

Scope note worth recording: the Veryfront adapter was already safe at the URL layer — veryfront-api-client/operations.ts:326,363,442,478 all use encodeURIComponent(pathOrId). So the traversal half of this PR delivers real value only for GitHub, and there it is incomplete.

P1 — hostile projectDir fails open to the local filesystem

src/platform/adapters/fs/integration.ts:67

The new guard rethrows only VeryfrontError with slug config-validation-failed. But PathNormalizer's constructor throws a plain TypeError (path-normalizer.ts:80-97), reached from veryfront/adapter.ts:270 during createFSAdapter.

Concrete failure: config sets fs.veryfront.projectDir = "/project/../etc". The constructor throws TypeError. integration.ts:67 does not match → falls through to :70-76 → logs "Falling back to local filesystem" → returns the unenhanced Deno adapter. The app now serves from the real local filesystem with no project scoping at all, silently, behind a warn line. Validation intended to harden the boundary instead removes it.

The config-validation-failed rethrow only covers the retry path (adapter-helpers.ts:33), which is what integration.test.ts:103-117 actually exercises. There is no test for the PathNormalizer-throw path.

P2 — previously-booting configs now fail to boot

src/platform/adapters/fs/veryfront/adapter-helpers.ts:22-30

normalizeFilesystemRetryConfig throws; it does not clamp (config-resource-limits.ts:70-84, 100-127). MAX_VERYFRONT_FILESYSTEM_RETRIES = 9.

Concrete failure: an existing app with fs.veryfront.retry.maxRetries: 10 boots fine on main (bare spread). After this PR: RangeErrorCONFIG_VALIDATION_FAILED → rethrown by adapter-helpers.ts:33 → rethrown by integration.ts:67, bypassing the local-fs fallback → enhanceAdapterWithFS rejects → the app fails to start. Same for initialDelay: 20000 against the default maxDelay: 10000.

Fail-fast is defensible, but this is a breaking runtime change framed as "hardening" with no migration note. Please scan deployed fs.veryfront.retry values for maxRetries > 9 or initialDelay > maxDelay before merging, and add a changelog entry.

P2 — the cache size-estimator guard is a no-op on the production backend

src/platform/adapters/fs/cache/size-estimator.ts:5-15

Stated goal: stop cyclic/BigInt/throwing-toJSON values propagating out of FileCache.set(). But file-cache.ts:201-217 computes estimateSize (now returning MAX_SAFE_INTEGER instead of throwing) and then calls JSON.stringify(entry) at :208, which rethrows the identical exception whenever a distributed backend is configured — production uses Upstash Redis. The guard only helps the in-memory path. Note setAsync at :241 does wrap the stringify in try/catch; the sync set does not.

Mitigating: these caches hold string, Uint8Array, DirectoryEntry[], FileInfo, string | null — all plain parsed-JSON data, so a cyclic value is not reachable today. Defence-in-depth against a hypothetical, implemented incompletely.

P3s

  • src/platform/adapters/fs/veryfront/types.ts:127-129retryDelayinitialDelay/maxDelay. I verified retryDelay was genuinely dead at the merge base (only types.ts:128 and README.md:240, no consumer), so the PR's claim is true and the change is correct. But FSAdapterConfig is exported, so a downstream app setting veryfront.retry.retryDelay gets a compile error on upgrade. Needs a semver/changelog note.
  • src/platform/adapters/fs/veryfront/retry.ts:37-40getSafeErrorMessage returns "" for non-native errors. An error built as Object.create(Error.prototype) satisfied main's instanceof Error and yielded error.message; it now yields "", so ECONNRESET-style matching silently stops working. Narrow — no such producer found in-repo.

What is correct — verified, worth keeping

  • Segment-boundary projectDir stripping (path-utils.ts:5-12, path-normalizer.ts:44-52). Main's bare startsWith genuinely mis-stripped /project/root against /project/root-other. Fixed and tested in both adapters.
  • Repo-scoped cache keys (cache-scope.ts) — correct, and the wiring is complete: all five bare-ref key sites updated (directory-operations.ts:21, read-operations.ts:41,61,100, stat-operations.ts:132,189), and buildGitHubTreeCacheKey already took repoId so was correctly left alone.
  • All six GitHub entry points normalize. The adapter is read-only (adapter.ts:97-127); no write/delete surface exists to miss, and no unguarded alternate path into getContents.
  • Symlink-skip preservation claim is true — the stat-operations.ts diff contains only cache-key changes.
  • The .-segment decision is well-reasoned and correctly documented. Retry status window 500–599 integer-bounded, and descriptor-based reads, are real improvements.

Test adequacy

Negative coverage exists and is decent for what it targets: path-utils.test.ts:53-75, path-normalizer.test.ts:104-153 (traversal, backslash, control chars, 4097-char bound, segment boundary), cross-repo cache isolation, integration.test.ts:103-117, size-estimator.test.ts:57-81.

The gaps map exactly onto the findings:

  1. No test for encoded traversal (%2e%2e, %2E%2E, .%2e) or backslash traversal on the GitHub side — the actual hole. The tests assert the literal form is blocked, which is precisely what produces the false confidence.
  2. No test at the sink. A single test doing new URL(baseUrl + endpoint) and asserting the pathname still starts with /repos/OWNER/REPO/ would have caught all four bypasses at once. That is the test to add.
  3. No test for the PathNormalizer-throw → local-fs fallback.
  4. No test that FileCache.set survives a cyclic value with a backend configured.

Production risk & rollback

Rollback clean — pure code, no migrations, no persisted-format change. The cache-key change means post-revert lookups miss once and refill; harmless.

  • Deploy-blocking: the retry-config validation can turn a running app into a boot failure. Check deployed configs first.
  • Security posture: net improvement over main, but the title and body would justify closing this as "traversal fixed" when it is not. If you merge before fixing, please amend the description so the residual bypass is not lost.
  • Silent failure: the local-fs fallback degrades isolation with only a warn line as signal.

Minimum to reach merge-ready

  1. Encode path segments at github-api-client.ts:62 and keep the .. rejection — verified above that neither alone is sufficient.
  2. Add the sink-level assertion test plus the encoded and backslash cases.
  3. Make integration.ts:67 rethrow path-validation failures too, or have PathNormalizer throw CONFIG_VALIDATION_FAILED.
  4. Either wrap the JSON.stringify at file-cache.ts:208 or drop the cache-guard claim.
  5. Changelog notes for the retry-config and retryDelay breaking changes.

@kwakayama kwakayama left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical Review — Score: 62/100

Verdict

The retry-boundary and cache-scoping work is careful, well-argued and well-tested, and the segment-boundary projectDir fix is a real bug fix. But the headline security claim does not hold: the GitHub traversal check rejects only path segments that are literally .., while the sink interpolates the path raw into a URL string, and the same WHATWG parsing behaviour the PR description relies on ("WHATWG URL resolution collapses dot segments") also collapses percent-encoded dots, treats backslashes as separators, and strips embedded tab/newline before dot-segment removal. The vector the PR was written to close is still open through three trivial encodings, and none of them are tested. Rubric band: 50–74, needs changes before merge.

Findings

  1. [blocker] normalizeGitHubPath is bypassable with %2e%2e, \, or embedded tab/newline — the exact vector C1 claims to close.
    src/platform/adapters/fs/github/path-utils.ts rejects only exact-match segments:

    for (const segment of collapsed.split("/")) {
      if (segment === ".") continue;
      if (segment === "..") { throw new TypeError(...); }

    The sink does no encoding — src/platform/adapters/fs/github/github-api-client.ts:63-65:

    const normalizedPath = path.replace(/^\/+/, "");
    const endpoint = `/repos/${owner}/${repo}/contents/${normalizedPath}?ref=${contentRef}`;

    followed by fetch(${this.baseUrl}${endpoint}) (line 136/140), i.e. a WHATWG URL parse of a string the caller controls. Per the URL Standard, a double-dot path segment is .. or an ASCII case-insensitive match for .%2e, %2e., or %2e%2e; a single-dot path segment is . or %2e. Separately, the basic URL parser removes all ASCII tab/LF/CR from the input before parsing, and for special schemes (https:) \ is a path separator. So all of these pass the new check and still collapse into traversal against a token-authenticated api.github.com request:

    • %2e%2e/%2e%2e/%2e%2e/user/repos (also %2E%2E, .%2e, %2e.)
    • ..\..\..\user\repos (one split("/") segment, never equal to ..)
    • .<TAB>./.<LF>./user/repos (segments are .\t., not .., until the parser strips the control chars)

    Reachability is unchanged from the PR's own analysis: readTextFilegetFileEntry miss (read-operations.ts:50) → readContentsFileclient.getContents(normalizedPath). The fix needs to reject on a decoded/parsed view (e.g. reject any % / \ / control char in a path segment, or encodeURIComponent each segment at the client, as the Veryfront client already does), and path-utils.test.ts needs cases for all three encodings — today it only tests literal ../.

  2. [major] The strict validator was put on the safe adapter and the lax one on the dangerous adapter.
    PathNormalizer.assertSafePath (fs/veryfront/path-normalizer.ts) rejects control characters, \, and >4096-char paths — but Veryfront paths are already made inert by encodeURIComponent(pathOrId) at every call site in src/platform/adapters/veryfront-api-client/operations.ts (lines 326, 363, 442, 480, 540, 578). normalizeGitHubPath has none of those checks, and it is the one whose output is interpolated unencoded (finding 1). The hardening is inverted relative to risk; at minimum the control-char/backslash/length checks belong in path-utils.ts too.

  3. [major] C2's guarded serialization does not cover the mode the file header calls production.
    size-estimator.ts now returns Number.MAX_SAFE_INTEGER instead of throwing, but FileCache.set() (fs/cache/file-cache.ts:206-215) does, in the distributed branch:

    const backend = this.getBackend();
    if (backend) {
      const serialized = JSON.stringify(entry);   // unguarded

    A cyclic value or BigInt still throws straight out of FileCache.set() whenever a Redis/API backend is active — precisely the failure the PR says it fixed. The "uncacheable, rejected by admission limits" claim also only holds for the memory path (setToFallback, line 255, size > this.options.maxMemory); in distributed mode the MAX_SAFE_INTEGER size is only used as a span attribute (line 249) and the entry is written anyway. Either guard line 208 the same way, or have set/setAsync skip admission when estimateSize returns the sentinel. No test covers the backend branch.

  4. [minor] Retry-config hardening is one-sided: the GitHub adapter's boundary is still unvalidated.
    buildRetryConfig now routes through normalizeFilesystemRetryConfig, but createGitHubConfig (fs/github/types.ts:103-107) still does a bare

    maxRetries: config.retry?.maxRetries ?? DEFAULT_MAX_RETRIES,

    and that value is passed as maxAttempts in github-api-client.ts:157 and :115. retryWithBackoff only rejects non-integers/< 1 (errors/error-handlers.ts:166-170), so {maxRetries: 1_000_000} is accepted and hammers the GitHub API — finite, but exactly the class of unbounded-budget bug the PR fixed on the sibling adapter. config-resource-limits.ts already ships MAX_GITHUB_FILESYSTEM_ATTEMPTS and the "legacy-total-attempts" semantics for this boundary; they are unused here.

  5. [minor] Cache-key scoping is incomplete and inconsistent with its own encoder.
    cache-scope.ts correctly encodeURIComponents owner:repo:ref, but the highest-value poisoning target — the whole file index — still uses the unencoded key at fs/github/stat-operations.ts:55: buildGitHubTreeCacheKey(this.client.repoId, this.config.ref) where repoId is `${owner}/${repo}`. {repo: "b:main", ref: "x"} and {repo: "b", ref: "main:x"} both produce github:tree:a/b:main:x. Same for github:blob:${sha} / github:blob:bytes:${sha} (read-operations.ts:195,209), left unscoped — defensible because git blob SHAs are content-addressed, but the PR text claims to enumerate the key-builder call sites and doesn't mention either. The path component of every key also stays unencoded, so a path containing :exact: can alias the bounded-read key built at read-operations.ts:99-101; the repo already has src/cache/keys/segment-codec.ts for exactly this.

  6. [minor] projectDir is stripped twice on the GitHub read/readdir paths.
    directory-operations.ts:20 normalizes, then passes the already-normalized path into statOps.isDirectory / getFilesInDirectory / getSubdirectories, each of which calls normalizeGitHubPath(path, this.projectDir) again (stat-operations.ts:224,228,243,259); read-operations.ts:50 does the same via getFileEntry. With projectDir: "app", /app/app/page.tsx normalizes to app/page.tsx and then to page.tsx — a different file — while the cache key (directory-operations.ts:21-24) was built from the once-normalized form, so key and lookup disagree. Pre-existing, but this PR is the one that rewrote projectDir stripping semantics and claims to have audited these call sites.

  7. [minor] integration.ts fail-fast covers one slug only.
    The new guard (fs/integration.ts:67-69) rethrows only config-validation-failed. A missing/invalid GitHub token throws CONFIG_INVALID (fs/github/adapter.ts:37,52), which still falls into the original catch and silently swaps the site onto denoAdapter — the "changing filesystems" hazard named by the new test (integration.test.ts, "should preserve invalid retry configuration instead of changing filesystems") remains open for every other configuration error.

  8. [nit] The new cache-isolation tests can pass vacuously.
    directory-operations.test.ts and read-operations.test.ts build new FileCache() and rely on the sync fallback. cacheBackend is module-global (file-cache.ts:49) and get() returns a miss unconditionally when a backend is set (file-cache.ts:134-137), so if any earlier test in the process calls initializeFileCacheBackend(), both assertions pass whether or not the scoping fix exists. Since the stated real-world exposure is the shared distributed backend, that path is the one left untested.

  9. [nit] new PathNormalizer("") makes every absolute path "in project".
    path-normalizer.ts guards with projectDir !== undefined, so an empty string yields projectDirPrefix === "" and then normalizedPath.startsWith(${projectDir}/) is startsWith("/") — true for any absolute path. The slice(0) is a no-op so behaviour is correct, but it logs "Converted absolute to relative path" for every read. Cheap fix: treat "" like undefined.

What's good

  • The retry-classification rewrite is genuinely correct: getOwnPropertyDescriptor-based reads, the integer 500–599 window, and the native-error/proxy checks close real getter-invocation and status: Infinity holes, and the three new tests in retry.test.ts pin exactly those behaviours. The single call site (file-list-access.ts:95, a file list) is idempotent and maxAttempts: 2 — no unbounded or non-idempotent retry anywhere in this diff.
  • Routing buildRetryConfig through the existing normalizeFilesystemRetryConfig rather than inventing new validation, plus dropping the vestigial retryDelay field from types.ts/README so overrides are actually expressible, is the right layering and removes a baseline typecheck exclusion.
  • The startsWith(projectDir) → segment-boundary fix is a real correctness bug fix in both adapters, and the /project/root vs /project/root-other tests pin it precisely.

🤖 Critical review by Claude Code

Merged via the queue into main with commit 73a6125 Aug 3, 2026
31 checks passed
@kojiwakayama
kojiwakayama deleted the fix/github-fs-path-hardening branch August 3, 2026 09:54
@kojiwakayama

Copy link
Copy Markdown
Contributor Author

The valid blockers in review 4842767810 were completed and verified, but #3315 entered the merge queue and merged before the follow-up commit could attach to its deleted head branch.

The code fixes are now isolated on current main in #3323:

  • authenticated GitHub contents URLs encode path segments and refs while retaining literal traversal rejection;
  • encoded-dot, backslash, control-character, and length cases are rejected;
  • invalid projectDir fails closed instead of switching to host-local storage;
  • synchronous distributed-cache serialization is contained and tested.

Fresh verification on current main: 172 focused steps, verify:quick, and the test-typecheck ratchet all pass. I also added the requested breaking-change migration/release note to this PR body, including the deployment-config audit requirement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants